Skip to content

WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model#377

Draft
JimBobSquarePants wants to merge 115 commits intomainfrom
js/canvas-api
Draft

WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model#377
JimBobSquarePants wants to merge 115 commits intomainfrom
js/canvas-api

Conversation

@JimBobSquarePants
Copy link
Member

@JimBobSquarePants JimBobSquarePants commented Mar 1, 2026

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Breaking Changes: DrawingCanvas API

Fix #106
Fix #244
Fix #344
Fix #367

This is a major breaking change. The library's public API has been completely redesigned around a canvas-based drawing model, replacing the previous collection of imperative extension methods.

What changed

The old API surface — dozens of IImageProcessingContext extension methods like DrawLine(), DrawPolygon(), FillPolygon(), DrawBeziers(), DrawImage(), DrawText(), etc. — has been removed entirely. These methods were individually simple but suffered from several architectural limitations:

  • Each call was an independent image processor that rasterized and composited in isolation, making it impossible to batch or reorder operations.
  • State (blending mode, clip paths, transforms) had to be passed to every single call.
  • There was no way for an alternate rendering backend to intercept or accelerate a sequence of draw calls.

The new model: DrawingCanvas

All drawing now goes through IDrawingCanvas / DrawingCanvas<TPixel>, a stateful canvas that queues draw commands and flushes them as a batch.

Via Image.Mutate() (most common)

using SixLabors.ImageSharp.Drawing;
using SixLabors.ImageSharp.Drawing.Processing;

image.Mutate(ctx => ctx.ProcessWithCanvas(canvas =>
{
    // Fill a path
    canvas.Fill(new EllipsePolygon(200, 200, 100), Brushes.Solid(Color.Red));

    // Stroke a path
    canvas.Draw(Pens.Solid(Color.Blue, 3), new RectangularPolygon(50, 50, 200, 100));

    // Draw a polyline
    canvas.DrawLine(Pens.Solid(Color.Green, 2), new PointF(0, 0), new PointF(100, 100));

    // Draw text
    canvas.DrawText(
        new RichTextOptions(font) { Origin = new PointF(10, 10) },
        "Hello, World!",
        Brushes.Solid(Color.Black),
        pen: null);

    // Draw an image
    canvas.DrawImage(sourceImage, sourceRect, destinationRect);

    // Save/Restore state (options, clip paths)
    canvas.Save(new DrawingOptions
    {
        GraphicsOptions = new GraphicsOptions { BlendPercentage = 0.5f }
    });
    canvas.Fill(path, brush);
    canvas.Restore();

    // Apply arbitrary image processing to a path region
    canvas.Process(path, inner => inner.Brightness(0.5f));

    // Commands are flushed on Dispose (or call canvas.Flush() explicitly)
}));

Standalone usage (without Image.Mutate)

DrawingCanvas<TPixel> can be constructed directly against an image frame, a Buffer2DRegion<TPixel>, or any ICanvasFrame<TPixel> implementation:

using var canvas = DrawingCanvas<Rgba32>.FromRootFrame(image, new DrawingOptions());

canvas.Fill(path, brush);
canvas.Draw(pen, path);
canvas.Flush();
using var canvas = DrawingCanvas<Rgba32>.FromImage(image, frameIndex: 0, new DrawingOptions());
// ...
using var canvas = DrawingCanvas<Rgba32>.FromFrame(frame, new DrawingOptions());
// ...
using var canvas = new DrawingCanvas<Rgba32>(configuration, cpuBufferRegion, new DrawingOptions());
// ...

Canvas state management

The canvas supports a save/restore stack (similar to HTML Canvas or SkCanvas):

canvas.Save(); // push current state
canvas.Save(options, clipPath1, clipPath2); // push and replace state

canvas.Restore();           // pop one level
canvas.RestoreTo(saveCount); // pop to a specific level

State includes DrawingOptions (graphics options, shape options, transform) and clip paths. CreateRegion() creates a child canvas over a sub-rectangle.

IDrawingBackend — bring your own renderer

The library's rasterization and composition pipeline is abstracted behind IDrawingBackend. This interface has the following methods:

Method Purpose
FlushCompositions<TPixel> Flushes queued composition operations for the target.
TryReadRegion<TPixel> Read pixels back from the target (needed for Process() and DrawImage())
ReleaseFrameResources<TPixel> Releases any backend resources cached against the specified target frame.

The library ships with DefaultDrawingBackend (CPU, tiled fixed-point rasterizer). An experimental WebGPU compute-shader backend (ImageSharp.Drawing.WebGPU) is also available, demonstrating how alternate backends plug in. Users will be able to provide their own implementations — for example, GPU-accelerated backends, SVG emitters, or recording/replay layers.

Backends are registered on Configuration:

// Will be possible once IDrawingBackend is public:
configuration.SetDrawingBackend(myCustomBackend);

Migration guide

Old API New API
ctx.Fill(color, path) ctx.ProcessWithCanvas(c => c.Fill(path, Brushes.Solid(color)))
ctx.Fill(brush, path) ctx.ProcessWithCanvas(c => c.Fill(path, brush))
ctx.Draw(pen, path) ctx.ProcessWithCanvas(c => c.Draw(pen, path))
ctx.DrawLine(pen, points) ctx.ProcessWithCanvas(c => c.DrawLine(pen, points))
ctx.DrawPolygon(pen, points) ctx.ProcessWithCanvas(c => c.Draw(pen, new Polygon(new LinearLineSegment(points))))
ctx.FillPolygon(brush, points) ctx.ProcessWithCanvas(c => c.Fill(new Polygon(new LinearLineSegment(points)), brush))
ctx.DrawText(text, font, color, origin) ctx.ProcessWithCanvas(c => c.DrawText(new RichTextOptions(font) { Origin = origin }, text, Brushes.Solid(color), null))
ctx.DrawImage(overlay, opacity) ctx.ProcessWithCanvas(c => c.DrawImage(overlay, sourceRect, destRect))
Multiple independent draw calls Single ProcessWithCanvas block — commands are batched and flushed together

Other breaking changes in this PR

  • AntialiasSubpixelDepth removed — The rasterizer now uses a fixed 256-step (8-bit) subpixel depth. The old AntialiasSubpixelDepth property (default: 16) controlled how many vertical subpixel steps the rasterizer used per pixel row. The new fixed-point scanline rasterizer integrates area/cover analytically per cell rather than sampling at discrete subpixel rows, so the "depth" is a property of the coordinate precision (24.8 fixed-point), not a tunable sample count. 256 steps gives ~0.4% coverage granularity — more than sufficient for all practical use cases. The old default of 16 (~6.25% granularity) could produce visible banding on gentle slopes.
  • GraphicsOptions.Antialias — now controls RasterizationMode (antialiased vs aliased). When false, coverage is snapped to binary using AntialiasThreshold.
  • GraphicsOptions.AntialiasThreshold — new property (0–1, default 0.5) controlling the coverage cutoff in aliased mode. Pixels with coverage at or above this value become fully opaque; pixels below are discarded. For users who previously set AntialiasSubpixelDepth = 1 to get aliased output, the equivalent is now Antialias = false.

Benchmarks

The DrawPolygonAll benchmar renders 7200x4800px path of the state of Mississippi with a 2px stroke.

Due to the fused design of our rasterizer, we're absolutely dominating.

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.26200
Unknown processor
.NET SDK=10.0.103
  [Host] : .NET 8.0.24 (8.0.2426.7010), X64 RyuJIT

Toolchain=InProcessEmitToolchain  InvocationCount=1  IterationCount=40
LaunchCount=3  UnrollFactor=1  WarmupCount=40
Method Mean Error StdDev Median Ratio RatioSD
SkiaSharp 37.30 ms 0.487 ms 1.510 ms 37.49 ms 1.00 0.00
SystemDrawing 44.03 ms 0.599 ms 1.935 ms 44.60 ms 1.19 0.08
ImageSharp 11.61 ms 0.231 ms 0.731 ms 11.56 ms 0.31 0.02
ImageSharpWebGPUNativeSurface 16.72 ms 0.283 ms 0.912 ms 16.63 ms 0.45 0.03

@JimBobSquarePants JimBobSquarePants changed the title WIP. Expand DrawingBackend API and Canvas. WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model/ Mar 7, 2026
@JimBobSquarePants JimBobSquarePants changed the title WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model/ WIP. DrawingCanvas API: Replace imperative extension methods with stateful canvas-based drawing model Mar 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants